Skip to content

docs(rfc): 插件扩展面盘点与边界(RFC 0011) - #385

Draft
NWYLZW wants to merge 9 commits into
mainfrom
claude/project-overview-qm2u8h
Draft

docs(rfc): 插件扩展面盘点与边界(RFC 0011)#385
NWYLZW wants to merge 9 commits into
mainfrom
claude/project-overview-qm2u8h

Conversation

@NWYLZW

@NWYLZW NWYLZW commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Refs #384

Summary

  • 新增 RFC 0011,按 rfc.md 的分章惯例拆五篇,为"插件到底能做什么"建立可引用的单一事实源
  • 0011-plugin-extensibility.md —— 总览、结论摘要、调研方法
  • 0011-plugin-extensibility-current-surface.md —— 现有扩展面盘点,逐条标注源码位置;含调研中三次误判的记录
  • 0011-plugin-extensibility-dsh-comparison.md —— 与 DeepSeek Harness / Cordis 的结构对照,钉在固定上游 revision
  • 0011-plugin-extensibility-boundaries.md —— 七条可引用的设计纪律
  • 0011-plugin-extensibility-actions.md —— 行动项,按是否需要产品决策分组
  • rfc.md 增加"插件扩展面 RFC"索引段

纯文档,不改任何运行时行为。

为什么需要这份 RFC

调研过程中对自身扩展面出现过三次错误判断,而调研是拿着完整代码库做的:

误判 实际情况
"插件之间不能声明依赖" children 是组合依赖;extensionPoints.onAvailable + pluginApis.call 是完整的运行时依赖装配,含等待语义、自动回收、epoch 竞态保护
"没有视图侧扩展点" toolUsePresentations 是完整的声明式渲染扩展,已在 cua-driver / browser-driver / external-browser-driver 生产使用
"agent loop 没有任何 seam" @oneworks/hooks 有 15 个事件,含 PreToolUse 否决权、GenerateSystemPrompt 改写权、continue: false 停机权

根因是能力面分散在手写文档(ui-runtime.md 400+ 行)、SKILL.md 与源码之间。误判记录刻意保留在 RFC 里,作为 P0-2「生成式能力目录」的论据。

七条纪律(供评审重点讨论)

  1. 插件不能创造插件 —— 动态性发生在配置解析层,不发生在插件代码里
  2. 视图扩展优先扩格式词汇表,而非开组件槽
  3. 注册型 seam 走常驻 runtime,不扩 hook 事件表
  4. 禁止 accepted-then-ignored
  5. trust / scope 字段的语义须明确写出(是逻辑隔离,不是安全边界)
  6. Model-visible ⟺ logged(建议采纳)
  7. capability seam 由 Definition / Provider / Consumer 三个 role 构成,单个 role 不构成 seam

Changelog

  • Not applicable(纯文档,不改产品代码)

Screenshots

Not applicable

Validation

  • pnpm dprint fmt 通过(格式化了 3 个文件,已随提交)
  • 脚本校验五篇 RFC 与 rfc.md 中全部相对链接可解析,无 broken link
  • 文档内引用的源码位置在撰写时逐条核对过;上游行号对应 deepseek-ai/deepseek-harness@99f6f02cordiverse/cordis@f46ae95,升级上游后需重新核对

Review Scope

  • Risk: low
  • Applicable profiles: 不涉及运行时,无 profile 影响
  • Evidence: 文档内每条结论均标注源码路径与行号;上游对照钉死 revision
  • Rule exceptions: none

评审建议聚焦两点:

  1. 盘点章节是否有事实错误或遗漏 —— 这份要当基线用,错了会一直错下去
  2. 七条纪律是否同意 —— 同意后即可作为后续新增扩展点的评审依据

行动项部分 P2 两项(model provider seam、适配器 seam 化)标注为"需要产品决策",本 PR 不推进,仅记录论据。

Experience Review

  • 已判断是否需要沉淀经验
  • 如需要,已运行 $post-task-experience-review
  • reviewer PASS / NOT APPLICABLE 后才进入 merge

Policy Conflict Review

  • Independent read-only reviewer checked workflow, permission, and release-rule conflicts and reported PASS

Generated by Claude Code

Establish a referenceable baseline for what the plugin system can actually
do today, compare it structurally against DeepSeek Harness / Cordis, and
record the boundary decisions that keep getting re-litigated.

- current-surface: inventory of the existing extension surface with source
  locations (config-layer plugin graph, extension points + plugin APIs,
  toolUsePresentations, the @oneworks/hooks middleware chain, server
  runtime primitives, security boundaries), plus a record of three
  misjudgements made during the survey
- dsh-comparison: structural comparison pinned to fixed upstream revisions,
  covering interception vs registration seams, external code-agent
  scheduling, and the generated-catalog documentation model
- boundaries: seven referenceable disciplines (plugins cannot create
  plugins, view extension ordering, registration seams belong on the
  resident runtime, no accepted-then-ignored, trust/scope semantics,
  model-visible implies logged, the three-role seam definition)
- actions: prioritised items split by whether they need a product decision

Docs only; no runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
oneworks-dev Ignored Ignored Preview Aug 20, 2026 2:34pm

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Experience Review

Before merge, make sure the PR body contains this completed checklist:

## Experience Review
- [x] 已判断是否需要沉淀经验
- [x] 如需要,已运行 `$post-task-experience-review`
- [x] reviewer `PASS` / `NOT APPLICABLE` 后才进入 merge

Checklist to confirm:

  • Decide whether this PR produced reusable project experience.
  • If experience should be captured, run $post-task-experience-review.
  • Merge only after the independent reviewer reports PASS or NOT APPLICABLE for the Experience Review Result.
  • For workflow, permission, or release-rule documentation changes, also add this checked section to the PR body after an independent read-only conflict review:
## Policy Conflict Review
- [x] Independent read-only reviewer checked workflow, permission, and release-rule conflicts and reported PASS

claude added 8 commits August 18, 2026 21:07
Collapse @oneworks/hooks and the plugin runtime into one extension surface.
The hook subprocess becomes a normalising reporter; plugin code moves into
whichever process drives the task, consuming one internal event stream.

- events-api: ctx.events with three modes narrowed from Cordis's five.
  emit/parallel/serial fold into `notify` (awaiting is the dispatcher's
  choice, not the event's); `waterfall` becomes `transform`; `bail` is
  replaced by `decide` — an order-independent, monotonically-tightening
  adjudication that encodes "capabilities add, permissions subtract" into
  dispatch semantics rather than leaving it to each event's implementation
- events: the vocabulary, renamed to DSH's namespace/kebab convention for
  migration parity, with per-source availability grading so unsupported
  subscriptions fail loud; four gap points identified against DSH, all in
  the model-request and around-dispatch layers
- runtime: reporter contract, endpoint resolution (no daemon needed — the
  process driving the task is alive by construction), permission layering
  where host baseline is synchronous and plugins can only tighten, and an
  explicit priority contract replacing the current array-order guarantee
- migration: five reversible steps, compat shim mapping for the old
  <pkg>/hooks entry, and an honest capability matrix for a DSH plugin shim

Also corrects the hook event count in RFC 0011 from 15 to 14.

Docs only; no runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
Revise RFC 0012's event API: instead of narrowing Cordis's five dispatch
modes down to three, keep all of them under their original names and move
the restrictions onto the event definition, where they can be checked
mechanically at define time.

Three earlier exclusions were wrong:

- `emit` was dropped by over-generalising "hook events cross a process
  boundary" to "all events do". Plugin-to-plugin events live in one
  runtime, where synchronous dispatch is both valid and preferable.
- `parallel` and `serial` were folded together on the premise that awaiting
  is the dispatcher's choice. That conflated two things: the real
  distinction is whether listeners can observe each other's side effects,
  which is a property of the event, not the call site.
- `bail` was banned for a real hazard applied too broadly. First-responder
  resolution is legitimate; only permission adjudication is unsafe, and
  that already has `decide`.

Constraints now: `emit` cannot be cross-process, and `security: true`
events accept only `decide`. Keeping Cordis's names also restores full
mode parity with DSH apart from `tools/pre-execute`, which is
`security: true` and therefore `decide` on our side.

Docs only; no runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The constraint stands but the stated reason was wrong. "Cross-process
cannot dispatch synchronously" is inaccurate: Atomics.wait() over a
SharedArrayBuffer with receiveMessageOnPort() gives real synchronous
blocking between worker_threads, and Node permits Atomics.wait on the
main thread. Only true separate processes are impractical, since a
SharedArrayBuffer cannot cross an IPC pipe.

The actual reason is semantic redundancy. What `emit` buys — no microtask
scheduling, listener on the dispatcher's stack so exceptions propagate
directly, deterministic uninterleaved order — is entirely lost once a
process boundary is crossed: the IPC round trip dwarfs scheduling cost,
the stack is already severed and errors must be serialised, and ordering
comes from serial transport rather than synchronous semantics. A blocking
notification is precisely `serial`, which already exists.

This rationale is transport-independent, so it survives a future move of
the hook worker from child_process to worker_threads.

Docs only; no runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The plugin system had no entry under .oo/rules/ — its normative content
was scattered across three RFC series, a hand-written usage doc, and the
code. Establish .oo/rules/PLUGIN-SYSTEM.md plus a plugin-system/ directory
following the same shape as HOOKS.md, so rules carry the specification and
RFCs stay as the record of why each decision was made.

This commit lands the entry, the overview, and the communication chapter.
The overview doubles as a gap map: every capability is tagged implemented,
in-design, or missing, so nobody writes plugins against a design.

Four inconsistencies surfaced while writing it, all recorded in the
overview:

- executeCommand accepts a scoped <scope>/<id> and will HTTP to another
  plugin's server command, while the usage doc describes only the
  same-scope path
- client can reach across plugins but server cannot: invokeChannel binds
  the scope host-side, so a server plugin only ever calls its own channel
- RFC 0012's three-level availability grading is coarser than reality —
  hooks/events.md already carries a per-adapter, per-event matrix with a
  canBlock dimension, and the event spec should defer to it
- same-scope command id collisions remain unverified

Remaining chapters are listed as pending rather than linked, so the index
has no dead links.

Docs only; no runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The spec was accumulating chapters without a spine — twelve documents
across two RFC series and a rules directory, each capability argued
separately, no single sentence saying what a plugin is. Give it one.

A plugin stands in exactly three relations to the host and to other
plugins, distinguished by who owns the result:

- contribute — hands over data, the receiver decides how to use it
- participate — gets called inside someone else's flow, the host merges
- provide — exposes a named capability, the plugin itself decides

The three are mutually exclusive and complete, so a new capability must
classify into one; if it doesn't, the model is wrong and changes first.
Crossed with where code runs (client / server / reporter), this yields a
matrix where the blanks are the gaps, and it reframes the open work in
one line each:

- contribute is healthy on both ends
- participate sits in the wrong place: it belongs on the resident server
  but currently runs in the reporter subprocess with a logger-only ctx,
  which is exactly what RFC 0012 moves
- provide is crippled server-side, since channels and registerApi bind
  scope host-side and a server plugin can only ever call its own

Chapters now follow the model: contribute.md, participate.md, provide.md
replace the earlier communication.md, whose content spanned two relations
and so obscured the distinction.

Docs only; no runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
The mode constraint table only covered participate-type channels, but
transport reachability and security grading are properties of every
communication channel, not just events.

- README: define `transport` and a three-tier `security` grading, plus
  where the security property attaches per relation (channel for
  contribute/participate, ctx capability surface for provide)
- contribute: asset directories are `advisory`, not `none` — skills and
  rules reach the system prompt, mcp reaches the toolset
- provide: provide-type channels carry no `security` field; the handler
  is the plugin's own code, so the real bound is what ctx exposes
- participate: frame the mode table as a specialization of the general
  rule rather than a standalone invention

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
README and provide.md both assert that the real bound on a plugin is the
ctx capability surface, with nothing behind the claim. This writes that
chapter.

The starting point is that there is no sandbox: client, server, and the
hook reporter all load plugin code through a plain dynamic import into
the host's own module graph. So "plugins cannot X" throughout the spec
means the host offers no channel for X, not that the host prevents it.

Documents the four boundaries that are real — the first-party capability
gate in createServerContext, the source-group whitelist that gate rests
on, the realpath-based scoped path check, and the loopback-only proxy
with credential header stripping — plus the build-time source boundary
and diagnostic redaction. Adds the review checklist for new ctx
capabilities, and an explicit list of guarantees we do not make.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
RFC 0011 surveyed the extension surface, 0012 converged hooks, and the
rules spec wrote both down. All three describe; none of them made any
code smaller. plugin-registry.ts still carries nine near-identical scope
filters, two separate pending queues, and contract checking in exactly
one place.

The kernel is one law: every named collection has a single end and a
many end, and the definer always holds the single end. Registry gives
the definer read and everyone contribute; event gives the definer emit
and everyone on; api gives the definer provide and everyone invoke. The
three relations stop being a taxonomy we declared and become a
consequence of who defined the collection.

Ownership, pending/drain, and contract checking move onto the collection
itself, so a new channel cannot forget them. `onAvailable` and the
`command` type both disappear — the first because waiting is now a
kernel property, the second because it is an api missing exactly the
four things collections provide by default.

Two edges stay deliberately outside: ctx construction and participant
instantiation. Admitting either is Cordis, and costs the guarantee that
the host baseline is the floor.

Includes the full interface surface and a six-phase migration where
phases 1-4 leave every existing plugin contract untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uDzTTAD3QqpHS8SHgRWEo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants